﻿using System;
using System.Diagnostics;
using System.Text;
using System.ComponentModel;
using System.Reflection;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;


namespace ModelingAddIns
{
    #region MetaFeature

    /// <summary>
    /// Encapsulates data and functionality required to install and instantiate a feature.
    /// </summary>
    /// <remarks>
    /// This is not an abstract class and thus can be used to represent features which don't require
    /// more data or special installation besides that provided here.
    /// </remarks>
    public class MetaFeature
    {
        private IMetaFeatureService m_store;
        private Type m_featureType;
        private FeatureAttribute m_featureData;
        private bool m_enabled = true;

        ////[CLSCompliant(false)]
        public MetaFeature(IMetaFeatureService store, Type featureType)
        {
            if (store == null) throw new ArgumentNullException("store");
            m_store = store;

            if (featureType == null) throw new ArgumentNullException("featureType");
            m_featureType = featureType;

            if (m_featureType.GetInterfaceMap(typeof(IDisposable)).InterfaceType == null)
                throw new ArgumentException("featureType runtime type must implement the System.IDisposable interface to be used as a feature type.");

            m_featureData = ReflectionHelper.GetAttribute<FeatureAttribute>(m_featureType);
            if (m_featureData == null)
                throw new ArgumentException("featureType must have a FeatureAttribute or derived applied.");

            Debug.Assert(m_featureData.MetaFeature == this.GetType());
        }

        /// <summary>
        /// Gets meta-feature store this meta-feature belongs to.
        /// </summary>
        //[CLSCompliant(false)]
        public IMetaFeatureService Store
        {
            [DebuggerStepThrough]
            get { return m_store; }
        }

        /// <summary>
        /// Gets runtime type of the feature class which is created when feature is instantiated.
        /// </summary>
        public Type FeatureType
        {
            [DebuggerStepThrough]
            get { return m_featureType; }
        }

        /// <summary>
        /// Gets the name of the feature to displayed in UI.
        /// </summary>
        /// <remarks>
        /// Default feature name is the name of its runtime type.
        /// </remarks>
        public virtual string Name
        {
            [DebuggerStepThrough]
            get { return m_featureData.Name != null ? m_featureData.Name : this.FeatureType.Name; }
        }

        /// <summary>
        /// Gets user-friendly description of the feature.
        /// </summary>
        public virtual string Description
        {
            [DebuggerStepThrough]
            get { return m_featureData.Description; }
        }

        /// <summary>
        /// Gets meta-feature within which this feature is contained.
        /// </summary>
        /// <remarks>
        /// At runtime parent feature is created before child feature is created and controls the lifetime
        /// of its child features. When parent is disposed, all its children are disposed as well.
        /// </remarks>
        public virtual Type ContainerMetaFeature
        {
            [DebuggerStepThrough]
            get { return m_featureData.Container; }
        }

        /// <summary>
        /// Gets installer capable of installing/uninstalling this meta-feature at runtime.
        /// </summary>
        public virtual IFeatureInstaller Installer
        {
            [DebuggerStepThrough]
            get { return DefaultFeatureInstaller.Instance; }
        }

        /// <summary>
        /// Gets or sets whether this feature is enabled.
        /// </summary>
        public bool Enabled
        {
            [DebuggerStepThrough]
            get { return m_enabled; }
            [DebuggerStepThrough]
            set { m_enabled = value; }
        }

        /// <summary>
        /// Creates feature of this meta-feature type in specified context.
        /// </summary>
        /// <param name="serviceProvider">The context in which the new feature is created.</param>
        /// <returns>Created feature.</returns>
        /// <remarks>
        /// Note that even though feature might choose to implement IFeature interface, this is not
        /// a requirement (whereas IDisposable is a requirement).
        /// </remarks>
        public IDisposable CreateFeature(ITypedServiceProvider serviceProvider)
        {
#if DEBUG
            EnvDTE.DTE dte = serviceProvider.GetService<EnvDTE.DTE>();
            dte.StatusBar.Text = "Loading " + this + "...";
#endif
            IDisposable disposableFeature = this.DoCreateFeature(serviceProvider);
            Debug.Assert(disposableFeature != null);

            IFeature feature = disposableFeature as IFeature;
            if (feature != null)
            {
                feature.Associate(this, serviceProvider);
            }

            Debug.WriteLine("Created feature " + this + ".");
            return disposableFeature;
        }

        /// <summary>
        /// Does the actual work of instantiating feature object before Associate is called
        /// from CreateFeature (if required).
        /// </summary>
        /// <param name="serviceProvider">Context the feature is being created in.</param>
        /// <returns>Created feature object.</returns>
        protected virtual IDisposable DoCreateFeature(ITypedServiceProvider serviceProvider)
        {
            if (serviceProvider == null) throw new ArgumentNullException("serviceProvider");

            // It is relatively common for types to take a IServiceProvider interface as an argument,
            // so we do provide a special case here for such situations (tool-windows and dialogs are
            // good examples of this).
            ConstructorInfo spConstructor = m_featureType.GetConstructor(new Type[] { typeof(IServiceProvider) });
            if (spConstructor != null)
            {
                return spConstructor.Invoke(new object[] { serviceProvider }) as IDisposable;
            }
            else
            {
                // If failed, assume the type has a defalut constructor.
                return Activator.CreateInstance(m_featureType) as IDisposable;
            }
        }

        /// <summary>
        /// Gets string representation of this instance.
        /// </summary>
        /// <returns>User-friendly string description of this meta-feature.</returns>
        public override string ToString()
        {
            return string.Format(CultureInfo.InvariantCulture,
                "{0} '{1}' ({2})",
                this.GetType().Name,
                this.m_featureData.Name,
                m_featureType.Name);
        }
    }

    #endregion

    #region FeatureInstaller

    /// <summary>
    /// Provides an abstraction on meta-feature installation functionality.
    /// </summary>
    public interface IFeatureInstaller
    {
        /// <summary>
        /// Installs the meta-feature into the product.
        /// </summary>
        /// <param name="metaFeature">Meta-feature to install.</param>
        void Install(MetaFeature metaFeature);

        /// <summary>
        /// Checks whether given meta-feature is already installed.
        /// </summary>
        /// <param name="metaFeature">Meta-feature.</param>
        /// <returns>True if installed and false otherwise.</returns>
        bool IsInstalled(MetaFeature metaFeature);

        /// <summary>
        /// Uninstalls specified meta-feature from the product.
        /// </summary>
        /// <param name="metaFeature">Meta-feature to uninstall.</param>
        void Uninstall(MetaFeature metaFeature);
    }

    /// <summary>
    /// Default implementation of the IFeatureInstaller interface which claims that
    /// a feature is always installed and does nothing on Install/Uninstall calls.
    /// </summary>
    internal sealed class DefaultFeatureInstaller : IFeatureInstaller
    {
        private DefaultFeatureInstaller() { }

        private static DefaultFeatureInstaller s_instance;
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        internal static DefaultFeatureInstaller Instance
        {
            [DebuggerStepThrough]
            get
            {
                if (s_instance == null)
                    s_instance = new DefaultFeatureInstaller();
                return s_instance;
            }
        }

        public void Install(MetaFeature metaFeature)
        {
            Debug.Assert(metaFeature != null);
            Debug.Assert(!this.IsInstalled(metaFeature));
        }

        public bool IsInstalled(MetaFeature metaFeature)
        {
            Debug.Assert(metaFeature != null);
            return true;
        }

        public void Uninstall(MetaFeature metaFeature)
        {
            Debug.Assert(metaFeature != null);
        }
    }

    #endregion

    #region FeatureAttribute

    /// <summary>
    /// Applying this attribute on a class makes that class a feature.
    /// A feature class must implement IDisposable interface.
    /// It can also optionally implement IFeature interface to receive context and meta-feature information on creation.
    /// </summary>
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
    [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes")]
    public class FeatureAttribute : Attribute
    {
        private string m_name;
        private string m_description;
        private Type m_container;

        /// <summary>
        /// User-friendly name of the feature.
        /// </summary>
        public string Name
        {
            [DebuggerStepThrough]
            get { return m_name; }
            [DebuggerStepThrough]
            set { m_name = value; }
        }

        /// <summary>
        /// User-friendly description of the feature.
        /// </summary>
        public string Description
        {
            [DebuggerStepThrough]
            get { return m_description != null ? m_description : string.Empty; }
            [DebuggerStepThrough]
            set { m_description = value; }
        }

        /// <summary>
        /// The type of container meta-feature (if missing, GlobalContext is assumed).
        /// </summary>
        public Type Container
        {
            [DebuggerStepThrough]
            get { return m_container != null ? m_container : typeof(GlobalContext); }
            [DebuggerStepThrough]
            set { m_container = value; }
        }

        /// <summary>
        /// The type of corresponding MetaFeature class (default is MetaFeature class itself).
        /// </summary>
        public virtual Type MetaFeature
        {
            [DebuggerStepThrough]
            get { return typeof(MetaFeature); }
        }
    }

    #endregion

    #region Feature

    /// <summary>
    /// Interface which feature classes implement in order to receive meta-feature and context
    /// information on creation. If a feature only needs the service provider, it may choose to
    /// just provide a public constructor with IServiceProvider argument.
    /// </summary>
    public interface IFeature : IDisposable
    {
        /// <summary>
        /// Called on a feature object right after it has been created from the
        /// meta-feature information.
        /// </summary>
        /// <param name="metaFeature">Corresponding MetaFeature which created the feature.</param>
        /// <param name="serviceProvider">Feature context (service provider).</param>
        void Associate(MetaFeature metaFeature, ITypedServiceProvider serviceProvider);
    }

    /// <summary>
    /// Base class for features not tied to other inhertiance hierarchies.
    /// </summary>
    public abstract class Feature : IFeature
    {
        private MetaFeature m_metaFeature;
        private ITypedServiceProvider m_serviceProvider;

        /// <summary>
        /// Called on a feature object right after it has been created from the
        /// meta-feature information.
        /// </summary>
        /// <param name="metaFeature">Corresponding MetaFeature which created the feature.</param>
        /// <param name="serviceProvider">Feature context (service provider).</param>
        public virtual void Associate(MetaFeature metaFeature, ITypedServiceProvider serviceProvider)
        {
            if (m_metaFeature != null || m_serviceProvider != null) throw new InvalidOperationException();

            if (metaFeature == null) throw new ArgumentNullException("metaFeature");
            m_metaFeature = metaFeature;

            // We do allow global context to have no parent context which is expected.
            if (serviceProvider == null) throw new ArgumentNullException("serviceProvider");
            m_serviceProvider = serviceProvider;
        }

        ~Feature()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            Debug.Assert(disposing, "Finalizing " + this.GetType().Name + " without disposing.");

            m_serviceProvider = null;
            m_metaFeature = null;

            Debug.WriteLine("Disposed feature of type: " + m_metaFeature);
        }

        /// <summary>
        /// Gets the meta-feature instance this feature was created from.
        /// </summary>
        public MetaFeature MetaFeature
        {
            [DebuggerStepThrough]
            get
            {
                Debug.Assert(m_metaFeature != null, "Accessing MetaFeature before Associate call.");
                return m_metaFeature;
            }
        }

        /// <summary>
        /// Gets context (IServiceProvider) to be used for service access by this feature.
        /// </summary>
        public ITypedServiceProvider ServiceProvider
        {
            [DebuggerStepThrough]
            get
            {
                Debug.Assert(m_serviceProvider != null, "Accessing Context before Associate call.");
                return m_serviceProvider;
            }
        }
    }

    #endregion

    #region ITypedServiceProvider

    public interface ITypedServiceProvider : IServiceProvider
    {
        /// <summary>
        /// Gets service of specified type.
        /// </summary>
        /// <typeparam name="T">The type of the service to retreive</typeparam>
        /// <returns>Retrieved service object or null if not supported.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
        T GetService<T>() where T : class;
    }

    #endregion

    #region IFeatureContext

    /// <summary>
    /// Provides environment context to a loaded feature.
    /// </summary>
    public interface IFeatureContext : ITypedServiceProvider
    {
        /// <summary>
        /// Searches for a feature of specified type in the current context only
        /// without looking up the context chain.
        /// </summary>
        /// <param name="featureType">Type of the feature to find.</param>
        /// <returns>Feature instance of found and null otherwise.</returns>
        object FindFeature(Type featureType);

        /// <summary>
        /// Searches for a feature of specified type in the current context only
        /// without looking up the context chain.
        /// </summary>
        /// <typeparam name="T">Type of the feature to find.</param>
        /// <returns>Feature instance of found and null otherwise.</returns>
        T FindFeature<T>() where T : class;

        /// <summary>
        /// Finds or if not found, creates an feature of specified type from the meta-store.
        /// </summary>
        /// <param name="featureType">Type of the feature to load.</param>
        /// <returns>Created feature.</returns>
        object CreateFeature(Type featureType);

        /// <summary>
        /// Finds or if not found, creates an feature of specified type from the meta-store.
        /// </summary>
        /// <typeparam name="T">Type of the feature to load.</param>
        /// <returns>Created feature.</returns>
        T CreateFeature<T>() where T : class;
    }

    #endregion

    #region FeatureGroup

    /// <summary>
    /// Base class for features which contain other features.
    /// </summary>
    public abstract class FeatureGroup : Feature, IFeatureContext
    {
        private Dictionary<MetaFeature, IDisposable> m_features;

        /// <summary>
        /// Called on a feature object right after it has been created from the
        /// meta-feature information.
        /// </summary>
        /// <param name="metaFeature">Corresponding MetaFeature which created the feature.</param>
        /// <param name="serviceProvider">Feature context (service provider).</param>
        public override void Associate(MetaFeature metaFeature, ITypedServiceProvider serviceProvider)
        {
            base.Associate(metaFeature, serviceProvider);

            IList<MetaFeature> children = metaFeature.Store.GetRolePlayers(metaFeature, MetaRelationshipKind.Child);
            Debug.Assert(children != null);
            if (children != null)
            {
                foreach (MetaFeature childMetaFeature in children)
                    if (childMetaFeature.Enabled)
                        this.CreateFeature(childMetaFeature.FeatureType);
            }
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        protected override void Dispose(bool disposing)
        {
            Exception disposeException = null;
            try
            {
                if (disposing)
                {
                    if (m_features != null)
                    {
                        foreach (KeyValuePair<MetaFeature, IDisposable> e in m_features)
                        {
                            try
                            {
                                e.Value.Dispose();
                            }
                            catch (Exception ex)
                            {
                                disposeException = ex;
                            }
                        }
                        m_features = null;
                    }
                }
            }
            finally
            {
                base.Dispose(disposing);

                if (disposeException != null)
                    throw disposeException;
            }
        }

        /// <summary>
        /// Gets service of specified type.
        /// </summary>
        /// <param name="serviceType">The type of the service to retreive</typeparam>
        /// <returns>Retrieved service object or null if not supported.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
        public virtual object GetService(Type serviceType)
        {
            if (serviceType == typeof(IFeatureContext))
                return this;
            else
                return this.ServiceProvider.GetService(serviceType);
        }

        /// <summary>
        /// Gets service of specified type.
        /// </summary>
        /// <typeparam name="T">The type of the service to retreive</typeparam>
        /// <returns>Retrieved service object or null if not supported.</returns>
        public T GetService<T>() where T : class
        {
            return this.GetService(typeof(T)) as T;
        }

        /// <summary>
        /// Searches for a feature of specified type in the current context only
        /// without looking up the context chain.
        /// </summary>
        /// <param name="featureType">Type of the feature to find.</param>
        /// <returns>Feature instance of found and null otherwise.</returns>
        public virtual object FindFeature(Type featureType)
        {
            if (m_features != null)
            {
                MetaFeature metaFeature = MetaFeature.Store.FindMetaFeature(featureType);
                if (metaFeature == null)
                    throw new ArgumentException("Meta-feature corrsponding to " + featureType + " was not found in the store.");

                IDisposable feature;
                if (m_features.TryGetValue(metaFeature, out feature))
                    return feature;
            }
            return null;
        }

        /// <summary>
        /// Searches for a feature of specified type in the current context only
        /// without looking up the context chain.
        /// </summary>
        /// <typeparam name="T">Type of the feature to find.</param>
        /// <returns>Feature instance of found and null otherwise.</returns>
        public T FindFeature<T>() where T : class
        {
            return this.FindFeature(typeof(T)) as T;
        }

        /// <summary>
        /// Finds or if not found, creates an feature of specified type from the meta-store.
        /// </summary>
        /// <param name="featureType">Type of the feature to load.</param>
        /// <returns>Created feature.</returns>
        public virtual object CreateFeature(Type featureType)
        {
            if (m_features == null)
                m_features = new Dictionary<MetaFeature, IDisposable>();

            MetaFeature metaFeature = MetaFeature.Store.FindMetaFeature(featureType);
            if (metaFeature == null)
                throw new ArgumentException("Meta-feature corrsponding to " + featureType + " was not found in the store.");

            if (!metaFeature.Enabled)
                return null;

            IDisposable feature;
            if (m_features.TryGetValue(metaFeature, out feature))
                return feature;
            else
                feature = metaFeature.CreateFeature(this);

            m_features[metaFeature] = feature;

            Debug.Assert(feature != null);
            return feature;
        }

        /// <summary>
        /// Finds or if not found, creates an feature of specified type from the meta-store.
        /// </summary>
        /// <typeparam name="T">Type of the feature to load.</param>
        /// <returns>Created feature.</returns>
        public T CreateFeature<T>() where T : class
        {
            return this.CreateFeature(typeof(T)) as T;
        }
    }

    #endregion

    #region GlobalContext

    /// <summary>
    /// Global context - the root of feature hierarchy.
    /// </summary>
    [Feature(Name = "Global Context", Description = "Top-level container for all features.")]
    public sealed class GlobalContext : FeatureGroup
    {
    }

    #endregion
}